diff options
Diffstat (limited to 'pages/en/anime/watch/[...info].js')
| -rw-r--r-- | pages/en/anime/watch/[...info].js | 598 |
1 files changed, 414 insertions, 184 deletions
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index aa0b672..f5b4fce 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -1,38 +1,156 @@ +import React, { useEffect, useRef, useState } from "react"; +import PlayerComponent from "@/components/watch/player/playerComponent"; +import { FlagIcon, ShareIcon } from "@heroicons/react/24/solid"; +import Details from "@/components/watch/primary/details"; +import EpisodeLists from "@/components/watch/secondary/episodeLists"; +import { getServerSession } from "next-auth"; +import { useWatchProvider } from "@/lib/hooks/watchPageProvider"; +import { authOptions } from "../../../api/auth/[...nextauth]"; +import { createList, createUser, getEpisode } from "@/prisma/user"; +import Link from "next/link"; +import MobileNav from "@/components/shared/MobileNav"; +import { NewNavbar } from "@/components/shared/NavBar"; +import Modal from "@/components/modal"; +import AniList from "@/components/media/aniList"; +import { signIn } from "next-auth/react"; +import BugReportForm from "@/components/shared/bugReport"; +import Skeleton from "react-loading-skeleton"; import Head from "next/head"; -import { useEffect, useState } from "react"; -import { getServerSession } from "next-auth/next"; -import { authOptions } from "../../../api/auth/[...nextauth]"; +export async function getServerSideProps(context) { + let userData = null; + const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; + + const query = context?.query; + if (!query) { + return { + notFound: true, + }; + } -import Navigasi from "../../../../components/home/staticNav"; -import PrimarySide from "../../../../components/anime/watch/primarySide"; -import SecondarySide from "../../../../components/anime/watch/secondarySide"; -import { createList, createUser, getEpisode } from "../../../../prisma/user"; + const proxy = process.env.PROXY_URI; + const disqus = process.env.DISQUS_SHORTNAME; -export default function Info({ - sessions, + const [aniId, provider] = query?.info; + const watchId = query?.id; + const epiNumber = query?.num; + const dub = query?.dub; + + const ress = await fetch(`https://graphql.anilist.co`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ + query: `query ($id: Int) { + Media (id: $id) { + mediaListEntry { + progress + status + customLists + repeat + } + id + idMal + title { + romaji + english + native + } + status + genres + episodes + studios { + edges { + node { + id + name + } + } + } + bannerImage + description + coverImage { + extraLarge + color + } + synonyms + + } + } + `, + variables: { + id: aniId, + }, + }), + }); + const data = await ress.json(); + + try { + if (session) { + await createUser(session.user.name); + await createList(session.user.name, watchId); + const data = await getEpisode(session.user.name, watchId); + userData = JSON.parse( + JSON.stringify(data, (key, value) => { + if (key === "createdDate") { + return String(value); + } + return value; + }) + ); + } + } catch (error) { + console.error(error); + // Handle the error here + } + return { + props: { + sessions: session, + provider: provider || null, + watchId: watchId || null, + epiNumber: epiNumber || null, + dub: dub || null, + userData: userData?.[0] || null, + info: data.data.Media || null, + proxy, + disqus, + }, + }; +} + +export default function Watch({ + info, watchId, - provider, - epiNumber, + disqus, + proxy, dub, - info, userData, - proxy, - disqus, + sessions, + provider, + epiNumber, }) { - const [currentEpisode, setCurrentEpisode] = useState(null); - const [loading, setLoading] = useState(false); - const [artStorage, setArtStorage] = useState(null); + + const [episodeNavigation, setEpisodeNavigation] = useState(null); const [episodesList, setepisodesList] = useState(); - const [mapProviders, setMapProviders] = useState(null); + const [mapEpisode, setMapEpisode] = useState(null); + + const [episodeSource, setEpisodeSource] = useState(null); + + const [open, setOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [onList, setOnList] = useState(false); - const [origin, setOrigin] = useState(null); + + const { theaterMode, setPlayerState, setAutoPlay, setMarked } = + useWatchProvider(); + + const playerRef = useRef(null); useEffect(() => { - setLoading(true); - setOrigin(window.location.origin); async function getInfo() { if (info.mediaListEntry) { setOnList(true); @@ -56,7 +174,7 @@ export default function Info({ }); } - setMapProviders(getMap?.episodes); + setMapEpisode(getMap?.episodes); } if (episodes) { @@ -79,207 +197,319 @@ export default function Info({ const previousEpisode = episodeList?.find( (i) => i.number === parseInt(epiNumber) - 1 ); - setCurrentEpisode({ + setEpisodeNavigation({ prev: previousEpisode, playing: { id: currentEpisode.id, title: playingData?.title, description: playingData?.description, - image: playingData?.image, + img: playingData?.img || playingData?.image, number: currentEpisode.number, }, next: nextEpisode, }); - } else { - setLoading(false); } } setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); // setEpiData(episodes); - setLoading(false); } getInfo(); return () => { - setCurrentEpisode(null); + setEpisodeNavigation(null); }; }, [sessions?.user?.name, epiNumber, dub]); + useEffect(() => { + async function fetchData() { + if (info) { + const autoplay = + localStorage.getItem("autoplay_video") === "true" ? true : false; + setAutoPlay(autoplay); + + const anify = await fetch("/api/v2/source", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + source: + provider === "gogoanime" && !watchId.startsWith("/") + ? "consumet" + : "anify", + providerId: provider, + watchId: watchId, + episode: epiNumber, + id: info.id, + sub: dub ? "dub" : "sub", + }), + }).then((res) => res.json()); + + const skip = await fetch( + `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( + epiNumber + )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=` + ).then((res) => { + if (!res.ok) { + switch (res.status) { + case 404: { + return null; + } + } + } + return res.json(); + }); + + const op = + skip?.results?.find((item) => item.skipType === "op") || null; + const ed = + skip?.results?.find((item) => item.skipType === "ed") || null; + + const episode = { + epiData: anify, + skip: { + op, + ed, + }, + }; + + setEpisodeSource(episode); + } + } + + fetchData(); + return () => { + setEpisodeSource(); + setPlayerState({ + currentTime: 0, + isPlaying: false, + }); + setMarked(0); + }; + }, [provider, watchId, info?.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `Watch Now - ${info?.title?.english || info.title.romaji}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + function handleOpen() { + setOpen(true); + document.body.style.overflow = "hidden"; + } + + function handleClose() { + setOpen(false); + document.body.style.overflow = "auto"; + } + return ( <> <Head> - <title>{info?.title?.romaji || "Retrieving data..."}</title> + <title> + {episodeNavigation?.playing?.title || + `${info?.title?.romaji} - Episode ${epiNumber}`} + </title> + {/* Write the best SEO for this watch page with data of anime title from info.title.romaji, episode title from episodeNavigation?.playing?.title, description from episodeNavigation?.playing?.description, episode number from epiNumber */} + <meta name="twitter:card" content="summary_large_image" /> + {/* Write the best SEO for this homepage */} <meta - name="title" - data-title-romaji={info?.title?.romaji} - data-title-english={info?.title?.english} - data-title-native={info?.title?.native} + name="description" + content={episodeNavigation?.playing?.description || info?.description} /> <meta - name="description" - content={currentEpisode?.playing?.description || info?.description} + name="keywords" + content="anime, anime streaming, anime streaming website, anime streaming free, anime streaming website free, anime streaming website free english subbed, anime streaming website free english dubbed, anime streaming website free english subbed and dubbed, anime streaming webs + ite free english subbed and dubbed download, anime streaming website free english subbed and dubbed" /> + <meta name="robots" content="index, follow" /> + + <meta property="og:type" content="website" /> + <meta property="og:url" content="https://moopa.live/" /> + <meta + property="og:title" + content={`Watch - ${ + episodeNavigation?.playing?.title || info?.title?.english + }`} + /> + <meta + property="og:description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" + /> + <meta property="og:image" content="/preview.png" /> + <meta property="og:site_name" content="Moopa" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" - content={`Episode ${epiNumber} - ${ - info.title.romaji || info.title.english + content={`Watch - ${ + episodeNavigation?.playing?.title || info?.title?.english }`} /> <meta name="twitter:description" - content={`${ - currentEpisode?.playing?.description?.slice(0, 180) || - info?.description?.slice(0, 180) - }...`} - /> - <meta - name="twitter:image" - content={`${origin}/api/og?title=${ - info.title.romaji || info.title.english - }&image=${info.bannerImage || info.coverImage.extraLarge}`} + content={episodeNavigation?.playing?.description || info?.description} /> </Head> + <Modal open={open} onClose={() => handleClose()}> + {!sessions && ( + <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> + <h1 className="text-md font-extrabold font-karla"> + Edit your list + </h1> + <button + className="flex items-center bg-[#363642] rounded-md text-white p-1" + onClick={() => signIn("AniListProvider")} + > + <h1 className="px-1 font-bold font-karla">Login with AniList</h1> + <div className="scale-[60%] pb-[1px]"> + <AniList /> + </div> + </button> + </div> + )} + </Modal> + <BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} /> + <main className="w-screen h-full"> + <NewNavbar + scrollP={20} + withNav={true} + shrink={true} + paddingY={`py-2 ${theaterMode ? "" : "lg:py-4"}`} + /> + <MobileNav hideProfile={true} sessions={sessions} /> + <div + className={`mx-auto pt-16 ${theaterMode ? "lg:pt-16" : "lg:pt-20"}`} + > + {theaterMode && ( + <PlayerComponent + id={"cinematic"} + session={sessions} + playerRef={playerRef} + dub={dub} + info={info} + watchId={watchId} + proxy={proxy} + track={episodeNavigation} + data={episodeSource?.epiData} + skip={episodeSource?.skip} + timeWatched={userData?.timeWatched} + provider={provider} + className="w-screen max-h-[85dvh]" + /> + )} + <div + id="default" + className={`${ + theaterMode ? "lg:max-w-[80%]" : "lg:max-w-[95%]" + } w-full flex flex-col lg:flex-row mx-auto`} + > + <div id="primary" className="w-full"> + {!theaterMode && ( + <PlayerComponent + id={"default"} + session={sessions} + playerRef={playerRef} + dub={dub} + info={info} + watchId={watchId} + proxy={proxy} + track={episodeNavigation} + data={episodeSource?.epiData} + skip={episodeSource?.skip} + timeWatched={userData?.timeWatched} + provider={provider} + /> + )} + <div + id="details" + className="flex flex-col gap-5 w-full px-3 lg:px-0" + > + <div className="flex items-end justify-between pt-3 border-b-2 border-secondary pb-2"> + <div className="w-[55%]"> + <div className="flex font-outfit font-semibold text-lg lg:text-2xl text-white line-clamp-1"> + <Link + href={`/en/anime/${info?.id}`} + className="hover:underline line-clamp-1" + > + {(episodeNavigation?.playing?.title || + info.title.romaji) ?? + "Loading..."} + </Link> + </div> + <p className="font-karla"> + {episodeNavigation?.playing?.number ? ( + `Episode ${episodeNavigation?.playing?.number}` + ) : ( + <Skeleton width={120} height={16} /> + )} + </p> + </div> + <div> + <div className="flex gap-2 text-sm"> + <button + type="button" + onClick={handleShareClick} + className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" + > + <ShareIcon className="w-5 h-5" /> + share + </button> + <button + type="button" + onClick={() => setIsOpen(true)} + className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" + > + <FlagIcon className="w-5 h-5" /> + report + </button> + </div> + </div> + {/* <div>right</div> */} + </div> - <Navigasi /> - <div className="w-screen flex justify-center my-3 lg:my-10"> - <div className="lg:w-[95%] flex flex-col lg:flex-row gap-5 lg:gap-0 justify-between"> - <PrimarySide - info={info} - navigation={currentEpisode} - episodeList={episodesList} - session={sessions} - epiNumber={epiNumber} - providerId={provider} - watchId={watchId} - onList={onList} - proxy={proxy} - disqus={disqus} - setOnList={setOnList} - setLoading={setLoading} - loading={loading} - timeWatched={userData?.timeWatched} - dub={dub} - /> - <SecondarySide - info={info} - map={mapProviders} - providerId={provider} - watchId={watchId} - episode={episodesList} - artStorage={artStorage} - dub={dub} - /> + <Details + info={info} + session={sessions} + description={info?.description} + epiNumber={epiNumber} + id={info} + onList={onList} + setOnList={setOnList} + handleOpen={() => handleOpen()} + disqus={disqus} + /> + </div> + </div> + <div + id="secondary" + className={`relative ${theaterMode ? "pt-2" : ""}`} + > + <EpisodeLists + info={info} + map={mapEpisode} + providerId={provider} + watchId={watchId} + episode={episodesList} + artStorage={artStorage} + dub={dub} + /> + </div> + </div> </div> - </div> + </main> </> ); } - -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - const accessToken = session?.user?.token || null; - - const query = context.query; - if (!query) { - return { - notFound: true, - }; - } - - const proxy = process.env.PROXY_URI; - const disqus = process.env.DISQUS_SHORTNAME; - - const [aniId, provider] = query.info; - const watchId = query.id; - const epiNumber = query.num; - const dub = query.dub; - - let userData = null; - - const ress = await fetch(`https://graphql.anilist.co`, { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, - body: JSON.stringify({ - query: `query ($id: Int) { - Media (id: $id) { - mediaListEntry { - progress - status - customLists - repeat - } - id - idMal - title { - romaji - english - native - } - status - genres - episodes - studios { - edges { - node { - id - name - } - } - } - bannerImage - description - coverImage { - extraLarge - color - } - synonyms - - } - } - `, - variables: { - id: aniId, - }, - }), - }); - const data = await ress.json(); - - try { - if (session) { - await createUser(session.user.name); - await createList(session.user.name, watchId); - const data = await getEpisode(session.user.name, watchId); - userData = JSON.parse( - JSON.stringify(data, (key, value) => { - if (key === "createdDate") { - return String(value); - } - return value; - }) - ); - } - } catch (error) { - console.error(error); - // Handle the error here - } - - return { - props: { - sessions: session, - aniId: aniId || null, - provider: provider || null, - watchId: watchId || null, - epiNumber: epiNumber || null, - dub: dub || null, - userData: userData?.[0] || null, - info: data.data.Media || null, - proxy, - disqus, - }, - }; -} |